web 开发中,经常遇到需要实时刷新数据的场景,例如即时聊天、新闻推送、数据监控等。 目前常用的实现方式共 5 种:
# 1. 短轮询(Polling)
短轮询实现原理是,客户端设置定时器,每隔一定时间向服务端发送 http 请求,服务端收到请求后,不管是否有新数据,都进行响应,响应完成后即关闭本次 TCP 连接。客户端代码实现:
setInterval(() => {
fetch(url).then((res) => {
// do something
})
}, 5000)
# 2. 长轮询(Long-Polling)
长轮询实现原理是,客户端发送 http 请求,服务端收到请求后,判断是否有新数据:
若有,则进行响应,客户端收到响应后,渲染数据并发起新的请求;
若无,则服务端 hold 连接,将请求挂起,请求处于 pending 状态,直到有数据或者超时才返回。
refreshData() { fetch(url).then((res) => { // do something refreshData() }).catch((error) => { // do something refreshData() }) }
# 3. 长连接(iframe)
长连接实现原理是,在页面中插入一个隐藏的 iframe 标签,利用 iframe 的 src 属性在客户端与服务端之间建立一个长连接,服务端向 iframe 传输数据(通常是<script>
标签包裹的 js 代码,动态将有效数据插入页面或更新页面)。
// 前端
<div id="refresh_wrap"></div>
<iframe src="/get-refresh-data" style="display: none;"></iframe>
// 后端
if (hasRefreshData) {
res.write(`
<script type="text/javascript">
parent.document.getElementById('refresh_wrap').innerHTML = "${data}"; // 改变父窗口dom元素
</script>
`)
}
# 4. 服务端推送事件(SSE, Server Sent Event)
严格来说,HTTP 协议是无法实现服务端推送的,但是当服务端向客户端声明接下来发送的是流信息时,连接将处于打开状态,SSE正是利用这一特性实现了服务端推送。
SSE在某写场景下可作为 WebSocket 的替代方案,二者对比如下:
SSE 是单向通道,仅能从服务端向客户端发送信息;
SSE 的实现成本更低,尤其是服务端;
SSE 不需要单独部署一套服务,而 WebSocket 需要单独部署;
// 前端 if (window.EventSource) { const source = new EventSource('/xxx/xxx') // 创建 EventSource 对象并连接服务器 // 连接成功后触发 open 事件 source.addEventListener('open', () => { console.log('Connected'); }, false); // 服务器发送信息到客户端时,如果没有 event 字段,默认会触发 message 事件 source.addEventListener('message', e => { console.log(`data: ${e.data}`); }, false); // 自定义 EventHandler,在收到 event 字段为 slide 的消息时触发 source.addEventListener('slide', e => { console.log(`data: ${e.data}`); // => data: 7 }, false); // 连接异常时会触发 error 事件并自动重连 source.addEventListener('error', e => { if (e.target.readyState === EventSource.CLOSED) { console.log('Disconnected'); } else if (e.target.readyState === EventSource.CONNECTING) { console.log('Connecting...'); } }, false); } // 服务端 const http = require('http'); http.createServer((req, res) => { // 服务器声明接下来发送的是事件流 res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', }); // 发送消息 setInterval(() => { res.write('event: slide\n'); // 事件类型 res.write(`id: ${+new Date()}\n`); // 消息 ID res.write('data: 7\n'); // 消息数据 res.write('retry: 10000\n'); // 重连时间 res.write('\n\n'); // 消息结束 }, 3000); // 发送注释保持长连接 setInterval(() => { res.write(': \n\n'); }, 12000); }).listen(2000);
# 5. WebSocket
# 简介
与http/https
协议仅能由客户端发起请求不同,WebSocket
既可以由客户端发起请求,也可以实现服务端主动向客户端推送信息,是一种真正的全双工通信协议。主要特点:
- 与
http/https
一样,建立在TCP
之上,服务端容易实现。 - 与
http/https
兼容良好,默认端口也是80/443
,因此握手阶段采用http/https
实现连接。 - 无同源限制,可与任何服务端通信,可取代 ajax。
- 协议标识符为
ws/wss
(对应http/https
),因此请求地址形如:ws://example.com:80/some/path
。
# 原理
如上所述,客户端发起WebSocket
连接请求,其握手阶段采用http/https
实现连接,但请求头与一般http/https
有所不同,类似下面这样:
GET / HTTP/1.1
Connection: Upgrade
Upgrade: websocket
Host: example.com
Origin: null
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
Connection
通知服务端,如果支持升级的话,进行协议升级。Upgrade
通知服务端,将协议由HTTP/1.1
升级到WebSocket
。Origin
提供请求发出的域名,供服务端权限校验(服务端也可不校验)。Sec-WebSocket-Key
提供握手协议的密钥,是 Base64 编码的 16 字节随机字符串。
服务端的WebSocket
响应类似如下:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Origin: null
Sec-WebSocket-Location: ws://example.com/
Connection
通知客户端,如果支持升级的话,进行协议升级。Upgrade
通知客户端,将协议由HTTP/1.1
升级到WebSocket
。Sec-WebSocket-Accept
提供握手协议的密钥,它是在Sec-WebSocket-Key
后边拼接特定字符串后生成的字符串,以便客户端校验是否是目标服务端响应了请求。Sec-WebSocket-Location
提供进行通信的WebSocket
地址。
完成握手以后,WebSocket
协议就在 TCP
协议之上建立了连接,开始传送数据。
# 应用
客户端示例
客户端使用WebSocket
无非进行如下操作:
- 建立连接和断开连接
- 发送数据和接收数据
- 处理错误
代码示例:
// 通过`http/https`握手,建立连接
var ws = new WebSocket('wss://echo.websocket.org');
// 连接成功后的回调
ws.onopen = function(evt) {
console.log('Connection open ...');
// 向服务端发送数据
ws.send('Hello WebSockets!');
};
// 收到服务端数据后的回调
ws.onmessage = function(evt) {
console.log('Received Message: ' + evt.data);
// 断开连接
ws.close();
};
// 连接断开后的回调
ws.onclose = function(evt) {
console.log('Connection closed.');
};
// 报错时的回调
ws.onerror = function(event) {
console.log('')
};
服务端
WebSocket
协议需要服务器支持。常用的 Node
实现有以下三种:
- µWebSockets
- Socket.IO
- WebSocket-Node
← 前端性能优化 实现多个标签页通信的方式 →